/** * Axelor Business Solutions * * Copyright (C) 2016 Axelor (<http://axelor.com>). * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.axelor.apps.account.service.payment; import java.io.DataInputStream; import java.io.FileInputStream; import java.io.FileReader; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.net.URLEncoder; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.Signature; import java.security.spec.X509EncodedKeySpec; import java.util.List; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.codec.Base64; import org.bouncycastle.util.io.pem.PemReader; import org.joda.time.DateTime; import org.joda.time.format.ISODateTimeFormat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.axelor.apps.account.db.AccountingSituation; import com.axelor.apps.account.db.PayboxConfig; import com.axelor.apps.account.db.PaymentVoucher; import com.axelor.apps.account.exception.IExceptionMessage; import com.axelor.apps.account.service.AccountingSituationService; import com.axelor.apps.account.service.config.PayboxConfigService; import com.axelor.apps.base.db.Company; import com.axelor.apps.base.db.Partner; import com.axelor.apps.base.db.repo.PartnerRepository; import com.axelor.apps.base.service.PartnerService; import com.axelor.apps.base.service.administration.GeneralServiceImpl; import com.axelor.apps.tool.StringTool; import com.axelor.exception.AxelorException; import com.axelor.exception.db.IException; import com.axelor.i18n.I18n; import com.axelor.inject.Beans; import com.google.inject.persist.Transactional; public class PayboxService { private final Logger log = LoggerFactory.getLogger( getClass() ); protected PayboxConfigService payboxConfigService; protected PartnerService partnerService; protected PartnerRepository partnerRepository; protected final String CHARSET = "UTF-8"; protected final String HASH_ENCRYPTION_ALGORITHM = "SHA1withRSA"; protected final String ENCRYPTION_ALGORITHM = "RSA"; public PayboxService(PayboxConfigService payboxConfigService, PartnerService partnerService, PartnerRepository partnerRepository) { this.payboxConfigService = payboxConfigService; this.partnerService = partnerService; this.partnerRepository = partnerRepository; } /** * Procédure permettant de réaliser un paiement avec Paybox * @param paymentVoucher * Une saisie paiement * @throws AxelorException * @throws UnsupportedEncodingException */ public String paybox(PaymentVoucher paymentVoucher) throws AxelorException, UnsupportedEncodingException { this.checkPayboxPaymentVoucherFields(paymentVoucher); Company company = paymentVoucher.getCompany(); BigDecimal paidAmount = paymentVoucher.getPaidAmount(); Partner payerPartner = paymentVoucher.getPartner(); // this.checkPayboxPartnerFields(payerPartner); this.checkPaidAmount(payerPartner, company, paidAmount); this.checkPaidAmount(paymentVoucher); PayboxConfig payboxConfig = payboxConfigService.getPayboxConfig(company); // Vérification du remplissage du chemin de la clé publique Paybox payboxConfigService.getPayboxPublicKeyPath(payboxConfig); String payboxUrl = payboxConfigService.getPayboxUrl(payboxConfig); String pbxSite = payboxConfigService.getPayboxSite(payboxConfig); String pbxRang = payboxConfigService.getPayboxRang(payboxConfig); String pbxDevise = payboxConfigService.getPayboxDevise(payboxConfig); String pbxTotal = paidAmount.setScale(2).toString().replace(".",""); String pbxCmd = paymentVoucher.getRef(); // Identifiant de la saisie paiement String pbxPorteur = this.getPartnerEmail(paymentVoucher); String pbxRetour = payboxConfigService.getPayboxRetour(payboxConfig); // String pbxEffectue = this.encodeUrl(this.replaceVariableInUrl(accountConfigService.getPayboxRetourUrlEffectue(accountConfig), paymentVoucher)); String pbxEffectue = this.replaceVariableInUrl(payboxConfigService.getPayboxRetourUrlEffectue(payboxConfig), paymentVoucher); String pbxRefuse = this.replaceVariableInUrl(payboxConfigService.getPayboxRetourUrlRefuse(payboxConfig), paymentVoucher); String pbxAnnule = this.replaceVariableInUrl(payboxConfigService.getPayboxRetourUrlAnnule(payboxConfig), paymentVoucher); String pbxIdentifiant = payboxConfigService.getPayboxIdentifiant(payboxConfig); String pbxHash = payboxConfigService.getPayboxHashSelect(payboxConfig); String pbxHmac = payboxConfigService.getPayboxHmac(payboxConfig); //Date à laquelle l'empreinte HMAC a été calculée (format ISO8601) String pbxTime = ISODateTimeFormat.dateHourMinuteSecond().print(new DateTime()); // Permet de restreindre les modes de paiement String pbxTypepaiement = "CARTE"; String pbxTypecarte = "CB"; String message = this.buildMessage(pbxSite, pbxRang, pbxIdentifiant, pbxTotal, pbxDevise, pbxCmd, pbxPorteur, pbxRetour, pbxEffectue, pbxRefuse, pbxAnnule, pbxHash, pbxTime, pbxTypepaiement, pbxTypecarte); log.debug("Message : {}",message); String messageHmac = this.getHmacSignature(message, pbxHmac, pbxHash); log.debug("Message HMAC : {}",messageHmac); String messageEncode = this.buildMessage( URLEncoder.encode(pbxSite, this.CHARSET), URLEncoder.encode(pbxRang, this.CHARSET), URLEncoder.encode(pbxIdentifiant, this.CHARSET), pbxTotal, URLEncoder.encode(pbxDevise, this.CHARSET), URLEncoder.encode(pbxCmd, this.CHARSET), URLEncoder.encode(pbxPorteur, this.CHARSET), URLEncoder.encode(pbxRetour, this.CHARSET), URLEncoder.encode(pbxEffectue, this.CHARSET), URLEncoder.encode(pbxRefuse, this.CHARSET), URLEncoder.encode(pbxAnnule, this.CHARSET), URLEncoder.encode(pbxHash, this.CHARSET), URLEncoder.encode(pbxTime, this.CHARSET), URLEncoder.encode(pbxTypepaiement, this.CHARSET), URLEncoder.encode(pbxTypecarte, this.CHARSET)); String url = payboxUrl + messageEncode + "&PBX_HMAC="+ messageHmac; log.debug("Url : {}",url); return url; } public String buildMessage(String pbxSite, String pbxRang,String pbxIdentifiant,String pbxTotal,String pbxDevise,String pbxCmd,String pbxPorteur,String pbxRetour, String pbxEffectue,String pbxRefuse,String pbxAnnule,String pbxHash,String pbxTime,String pbxTypepaiement,String pbxTypecarte) { return String.format("PBX_SITE=%s&PBX_RANG=%s&PBX_IDENTIFIANT=%s&PBX_TOTAL=%s&PBX_DEVISE=%s" + "&PBX_CMD=%s&PBX_PORTEUR=%s&PBX_RETOUR=%s&PBX_EFFECTUE=%s&PBX_REFUSE=%s&PBX_ANNULE=%s&PBX_HASH=%s&PBX_TIME=%s&PBX_TYPEPAIEMENT=%s&PBX_TYPECARTE=%s", pbxSite, pbxRang, pbxIdentifiant, pbxTotal, pbxDevise, pbxCmd, pbxPorteur, pbxRetour, pbxEffectue, pbxRefuse, pbxAnnule, pbxHash, pbxTime, pbxTypepaiement, pbxTypecarte); } /** * Fonction remplaçant le paramètre %id par le numéro d'id de la saisie paiement * @param url * @param paymentVoucher * @return */ public String replaceVariableInUrl(String url, PaymentVoucher paymentVoucher) { return url.replaceAll("%idPV", paymentVoucher.getId().toString()); } /** * Fonction convertissant l'url en url encodé * @param url * @return */ public String encodeUrl(String url) { String newUrl = url.replaceAll("\\%", "%25"); newUrl = newUrl.replaceAll("\\?", "%3F"); newUrl = newUrl.replaceAll("\\/", "%2F"); newUrl = newUrl.replaceAll("\\:", "%3A"); newUrl = newUrl.replaceAll("\\#", "%23"); newUrl = newUrl.replaceAll("\\&", "%26"); newUrl = newUrl.replaceAll("\\=", "%3D"); newUrl = newUrl.replaceAll("\\+", "%2B"); newUrl = newUrl.replaceAll("\\$", "%24"); newUrl = newUrl.replaceAll("\\,", "%2C"); newUrl = newUrl.replaceAll(" ", "%20"); newUrl = newUrl.replaceAll("\\;", "%3B"); newUrl = newUrl.replaceAll("\\<", "%3C"); newUrl = newUrl.replaceAll("\\>", "%3E"); newUrl = newUrl.replaceAll("\\~", "%7E"); newUrl = newUrl.replaceAll("\\.", "%2E"); return newUrl; // return url; } public String getPartnerEmail(PaymentVoucher paymentVoucher) throws AxelorException { Partner partner = paymentVoucher.getPartner(); Company company = paymentVoucher.getCompany(); if(partner.getEmailAddress().getAddress() != null && !partner.getEmailAddress().getAddress().isEmpty()) { return partner.getEmailAddress().getAddress(); } else if(paymentVoucher.getEmail() != null && !paymentVoucher.getEmail().isEmpty()) { return paymentVoucher.getEmail(); } else { return payboxConfigService.getPayboxDefaultEmail(payboxConfigService.getPayboxConfig(company)); } } /** * Procédure permettant de vérifier que les champs de la saisie paiement necessaire à Paybox sont bien remplis * @param paymentVoucher * @throws AxelorException */ public void checkPayboxPaymentVoucherFields(PaymentVoucher paymentVoucher) throws AxelorException { if (paymentVoucher.getPaidAmount() == null || paymentVoucher.getPaidAmount().compareTo(BigDecimal.ZERO) > 1) { throw new AxelorException(String.format(I18n.get(IExceptionMessage.PAYBOX_1), GeneralServiceImpl.EXCEPTION, paymentVoucher.getRef()), IException.CONFIGURATION_ERROR); } } /** * Procédure permettant de vérifier que le montant réglé par Paybox n'est pas supérieur au solde du payeur * @param partner * @param paidAmount * @throws AxelorException */ public void checkPaidAmount(Partner partner, Company company, BigDecimal paidAmount) throws AxelorException { AccountingSituation accountingSituation = Beans.get(AccountingSituationService.class).getAccountingSituation(partner, company); BigDecimal partnerBalance = accountingSituation.getBalanceCustAccount(); if(paidAmount.compareTo(partnerBalance) > 0) { throw new AxelorException(String.format(I18n.get(IExceptionMessage.PAYBOX_2), GeneralServiceImpl.EXCEPTION), IException.CONFIGURATION_ERROR); } } public void checkPaidAmount(PaymentVoucher paymentVoucher) throws AxelorException { if(paymentVoucher.getRemainingAmount().compareTo(BigDecimal.ZERO) > 0 ) { throw new AxelorException(String.format(I18n.get(IExceptionMessage.PAYBOX_3), GeneralServiceImpl.EXCEPTION), IException.INCONSISTENCY); } } /** * Procédure permettant de vérifier que le paramétrage des champs necessaire à Paybox d'un tiers est bien réalisé * @param partner * @throws AxelorException */ public void checkPayboxPartnerFields(Partner partner) throws AxelorException { if (partner.getEmailAddress().getAddress() == null || partner.getEmailAddress().getAddress().isEmpty()) { throw new AxelorException(String.format(I18n.get(IExceptionMessage.PAYBOX_4), GeneralServiceImpl.EXCEPTION, partner.getName()), IException.CONFIGURATION_ERROR); } } /** * Fonction calculant la signature HMAC des paramètres * @param data * La chaine contenant les paramètres * @param hmacKey * La clé HMAC * @param algorithm * L'algorithme utilisé (SHA512, ...) * @return * @throws AxelorException */ public String getHmacSignature(String data, String hmacKey, String algorithm) throws AxelorException { try { byte[] bytesKey = DatatypeConverter.parseHexBinary(hmacKey); SecretKeySpec secretKey = new SecretKeySpec(bytesKey, "Hmac"+algorithm); Mac mac = Mac.getInstance("Hmac"+algorithm); mac.init(secretKey); byte[] macData = mac.doFinal( data.getBytes(this.CHARSET) ); // final byte[] hex = new Hex().encode( macData ); // return new String( hex, this.CHARSET ); // LOG.debug("Message HMAC 2 : {}",new String( hex, this.CHARSET )); String s = StringTool.getHexString(macData); return s.toUpperCase(); } catch (InvalidKeyException e) { throw new AxelorException(String.format("%s :\n %s", GeneralServiceImpl.EXCEPTION,e), IException.INCONSISTENCY); } catch (NoSuchAlgorithmException e) { throw new AxelorException(String.format("%s :\n %s", GeneralServiceImpl.EXCEPTION,e), IException.INCONSISTENCY); } catch (UnsupportedEncodingException e) { throw new AxelorException(String.format("%s :\n %s", GeneralServiceImpl.EXCEPTION,e), IException.INCONSISTENCY); } } /** * Méthode permettant d'ajouter une adresse email à un contact * @param contact * Un contact * @param email * Une adresse email * @param toSaveOk * L'adresse email doit-elle être enregistré pour le contact */ @Transactional(rollbackOn = {AxelorException.class, Exception.class}) public void addPayboxEmail(Partner partner, String email, boolean toSaveOk) { if(toSaveOk) { partner.getEmailAddress().setAddress(email); partnerRepository.save(partner); } } /** * * @param signature * La signture contenu dans l'url * @param varUrl * Liste des variables contenu dans l'url, privé de la dernière : la signature * @param company * La société * @return * @throws Exception */ public boolean checkPaybox(String signature, List<String> varUrl, Company company) throws Exception { boolean result = this.checkPaybox(signature, varUrl, company.getAccountConfig().getPayboxConfig().getPayboxPublicKeyPath()); log.debug("Resultat de la verification de signature : {}",result); return result; } /** * * @param signature * La signature contenu dans l'url * @param urlParam * Liste des paramètres contenus dans l'url, privé du dernier : la signature * @param pubKeyPath * Le chemin de la clé publique Paybox * @return * @throws Exception */ public boolean checkPaybox(String signature, List<String> urlParam, String pubKeyPath) throws Exception { String payboxParams = StringUtils.join(urlParam, "&"); log.debug("Liste des variables Paybox signées : {}",payboxParams); // Déjà décodée par le framework // String decoded = URLDecoder.decode(sign, this.CHARSET); byte[] sigBytes = Base64.decode(signature.getBytes(this.CHARSET)); // lecture de la cle publique PublicKey pubKey = this.getPubKey(pubKeyPath); /** * Dans le cas où le clé est au format .der * * PublicKey pubKey = this.getPubKeyDer(pubKeyPath); */ // verification signature return this.verify(payboxParams.getBytes(), sigBytes, this.HASH_ENCRYPTION_ALGORITHM, pubKey); } /** Chargement de la cle AU FORMAT pem * Alors ajouter la dépendance dans le fichier pom.xml : * <dependency> * <groupId>org.bouncycastle</groupId> * <artifactId>bcprov-jdk15on</artifactId> * <version>1.47</version> * </dependency> * * Ainsi que l'import : import org.bouncycastle.util.io.pem.PemReader; * * @param pubKeyFile * @return * @throws Exception */ private PublicKey getPubKey(String pubKeyPath) throws Exception { PemReader reader = new PemReader(new FileReader(pubKeyPath)); byte[] pubKey = reader.readPemObject().getContent(); reader.close(); KeyFactory keyFactory = KeyFactory.getInstance(this.ENCRYPTION_ALGORITHM); X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(pubKey); return keyFactory.generatePublic(pubKeySpec); } /** Chargement de la cle AU FORMAT der * Utliser la commande suivante pour 'convertir' la clé 'pem' en 'der' * openssl rsa -inform PEM -in pubkey.pem -outform DER -pubin -out pubkey.der * * @param pubKeyFile * @return * @throws Exception */ @Deprecated private PublicKey getPubKeyDer(String pubKeyPath) throws Exception { FileInputStream fis = new FileInputStream(pubKeyPath); DataInputStream dis = new DataInputStream(fis); byte[] pubKeyBytes = new byte[fis.available()]; dis.readFully(pubKeyBytes); fis.close(); dis.close(); KeyFactory keyFactory = KeyFactory.getInstance(this.ENCRYPTION_ALGORITHM); // extraction cle X509EncodedKeySpec pubSpec = new X509EncodedKeySpec(pubKeyBytes); return keyFactory.generatePublic(pubSpec); } /** * verification signature RSA des donnees avec cle publique * @param dataBytes * @param sigBytes * @param pubKey * @return * @throws Exception */ private boolean verify( byte[] dataBytes, byte[] sigBytes, String sigAlg, PublicKey pubKey) throws Exception { Signature signature = Signature.getInstance(sigAlg); signature.initVerify(pubKey); signature.update(dataBytes); return signature.verify(sigBytes); } }